﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Ue.Core.FileSystems;

namespace Ue.Eagle.Runtime.FileSystems.Dependencies
{
    public class DependenciesFolder : IDependenciesFolder
    {
        private const string BasePath = "Dependencies";
        private const string FileName = "dependencies.xml";
        private readonly IAppDataFolder appDataFolder;

        public DependenciesFolder(IAppDataFolder appDataFolder)
        {
            this.appDataFolder = appDataFolder;
        }

        private string PersistencePath
        {
            get { return this.appDataFolder.Combine(BasePath, FileName); }
        }

        public DependencyDescriptor GetDescriptor(string moduleName)
        {
            return this.LoadDescriptors().SingleOrDefault(d => StringComparer.OrdinalIgnoreCase.Equals(d.Name, moduleName));
        }

        public IEnumerable<DependencyDescriptor> LoadDescriptors()
        {
            return this.ReadDependencies(this.PersistencePath);
        }

        public void StoreDescriptors(IEnumerable<DependencyDescriptor> dependencyDescriptors)
        {
            var existingDescriptors = this.LoadDescriptors().OrderBy(d => d.Name);
            var newDescriptors = dependencyDescriptors.OrderBy(d => d.Name);

            if (!newDescriptors.SequenceEqual(existingDescriptors, new DependencyDescriptorComparer()))
            {
                this.WriteDependencies(this.PersistencePath, dependencyDescriptors);
            }
        }

        private IEnumerable<DependencyDescriptor> ReadDependencies(string path)
        {
            Func<string, XName> ns = (name => XName.Get(name));
            Func<XElement, string, string> elem = (e, name) => e.Element(ns(name)).Value;

            if (!this.appDataFolder.FileExists(path))
            {
                return Enumerable.Empty<DependencyDescriptor>();
            }

            using (var stream = this.appDataFolder.OpenFile(path))
            {
                var document = XDocument.Load(stream);

                return document
                    .Elements(ns("Dependencies"))
                    .Elements(ns("Dependency"))
                    .Select(e =>
                        new DependencyDescriptor()
                        {
                            Name = elem(e, "ModuleName"),
                            LoaderName = elem(e, "LoaderName"),
                            VirtualPath = elem(e, "VirtualPath"),
                            References = e.Elements(ns("References")).Elements("Reference").Select(r =>
                                new DependencyReferenceDescriptor()
                                {
                                    Name = elem(r, "Name"),
                                    LoaderName = elem(r, "LoaderName"),
                                    VirtualPath = elem(r, "VirtualPath")
                                }
                            )
                        }
                    );
            }
        }

        private void WriteDependencies(string path, IEnumerable<DependencyDescriptor> dependencyDescriptors)
        {
            Func<string, XName> ns = (name => XName.Get(name));

            var document = new XDocument();
            document.Add(new XElement(ns("Dependencies")));

            var elements = dependencyDescriptors
                .Select(d => 
                    new XElement(ns("Dependency"),
                        new XElement(ns("ModuleName"), d.Name),
                        new XElement(ns("LoaderName"), d.LoaderName),
                        new XElement(ns("VirtualPath"), d.VirtualPath),
                        new XElement(ns("References"),d.References.Select(r=>
                            new XElement(ns("Reference"),
                                new XElement(ns("Name"), r.Name),
                                new XElement(ns("LoaderName"), r.LoaderName),
                                new XElement(ns("VirtualPath"), r.VirtualPath))
                            )
                        )
                    )
                );

            document.Root.Add(elements.ToArray());

            using (var stream = this.appDataFolder.CreateFile(path))
            {
                document.Save(stream, SaveOptions.None);
            }
        }

        private class DependencyDescriptorComparer : EqualityComparer<DependencyDescriptor>
        {
            private readonly ReferenceDescriptorComparer _referenceDescriptorComparer = new ReferenceDescriptorComparer();

            public override bool Equals(DependencyDescriptor x, DependencyDescriptor y)
            {
                return
                    StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
                    StringComparer.OrdinalIgnoreCase.Equals(x.LoaderName, y.LoaderName) &&
                    StringComparer.OrdinalIgnoreCase.Equals(x.VirtualPath, y.VirtualPath) &&
                    x.References.SequenceEqual(y.References, _referenceDescriptorComparer);
            }

            public override int GetHashCode(DependencyDescriptor obj)
            {
                return
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name) ^
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.LoaderName) ^
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.VirtualPath) ^
                    obj.References.Aggregate(0, (a, entry) => a + _referenceDescriptorComparer.GetHashCode(entry));
            }
        }

        private class ReferenceDescriptorComparer : EqualityComparer<DependencyReferenceDescriptor>
        {
            public override bool Equals(DependencyReferenceDescriptor x, DependencyReferenceDescriptor y)
            {
                return
                    StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
                    StringComparer.OrdinalIgnoreCase.Equals(x.LoaderName, y.LoaderName) &&
                    StringComparer.OrdinalIgnoreCase.Equals(x.VirtualPath, y.VirtualPath);

            }

            public override int GetHashCode(DependencyReferenceDescriptor obj)
            {
                return
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name) ^
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.LoaderName) ^
                    StringComparer.OrdinalIgnoreCase.GetHashCode(obj.VirtualPath);
            }
        }
    }
}
